﻿using System;
using System.Collections.Generic;
using System.Linq;
using PSE.Updater.Common.Gateway.DataContract;
using PSE.Updater.Common.Providers.Interfaces;
using PSE.Updater.Common.Configurations.Server;
using PSE.Updater.Common.Providers;
using PSE.Framework.ErrorLogging;

namespace PSE.Updater.Server.Engine.Providers.Updates
{
    /// <summary>
    /// Classe responsável por gerenciar a disponibilização de atualizações
    /// </summary>
    internal sealed class UpdateDiscovery : IUpdateDiscovery
    {
        private CacheManager _cacheManager;

        #region [ Singleton ]
        private static object _singletonLock = new object();
        private static UpdateDiscovery _updateDiscovery;
        public static UpdateDiscovery Instance
        {
            get
            {
                lock (_singletonLock)
                {
                    if (_updateDiscovery == null)
                        _updateDiscovery = new UpdateDiscovery();

                    return _updateDiscovery;
                }

            }
        }
        #endregion [ Singleton ]

        /// <summary>
        /// Construtor
        /// </summary>
        private UpdateDiscovery()
        {
            _cacheManager = CacheManager.Instance;
        }

        /// <summary>
        /// Obtém todos os arquivos disponíveis para a avaliação de Atualização
        /// </summary>
        /// <returns>Todos os arquivos disponíveis para a avaliação de Atualização</returns>
        public IList<IFileDetails> GetAllFiles(string productName)
        {
            return _cacheManager.GetCopyList(productName);
        }

        public IList<IFileDetails> GetAvailableUpdates(ProviderType type, List<ModuleVersion> moduleVersions)
        {
            if (moduleVersions == null || moduleVersions.Count == 0)
                throw new ArgumentNullException("moduleVersions");

            List<IFileDetails> files = new List<IFileDetails>();
            foreach (var module in moduleVersions)
            {
                DebugLog.Write("Verificando se existem atualizações para o módulo " + module.ModuleName + " com a versão " + module.Version);
                var avaliableUpdates = GetAvailableUpdates(type, module.SystemName, module.ModuleName, module.Version);
                if (avaliableUpdates != null)
                {
                    if (avaliableUpdates.Count > 0)
                    {
                        DebugLog.Write("Existem atualizações para o módulo " + module.ModuleName + " com a versão " + module.Version);
                        files.AddRange(avaliableUpdates);
                    }
                }
                else
                {
                    DebugLog.Write("Não existem atualizações para o módulo " + module.ModuleName + " com a versão " + module.Version);
                }
            }
            var distinct = files.Distinct(new ComparePackage()).ToList();
            return distinct;
        }

        /// <summary>
        /// Obtém as Atualizações disponíveis
        /// </summary>
        /// <param name="productName">Nome do Produto</param>
        /// <param name="moduleName">Nome do Módulo</param>
        /// <param name="version">Versão do Módulo</param>
        /// <returns>Atualizações disponíveis</returns>
        private IList<IFileDetails> GetAvailableUpdates(ProviderType type, string productName, string moduleName, string version)
        {
            if (string.IsNullOrEmpty(productName))
                throw new ArgumentNullException("productName");

            if (string.IsNullOrEmpty(moduleName))
                throw new ArgumentNullException("moduleName");

            if (string.IsNullOrEmpty(version))
                throw new ArgumentNullException("version");

            PSE.Framework.Common.Version pseVersion = CreateVersion(version);

            List<IFileDetails> packages = _cacheManager.GetCopyList(type, productName, moduleName);
            if (packages != null && packages.Count > 0)
            {
                DebugLog.Write(packages.Count.ToString() + " pacotes no cache com o módulo " + moduleName);

                //recuperar todos os pacotes com versao maior que a versao informada, e estao dentro de uma mesma tecnologia e release
                //ATENCAO: mudar essa linha de codigo pode impactar em toda a logica do metodo
                List<IFileDetails> availablePackages = packages.FindAll(p =>
                {
                    /*return (p.Version.Major == version.Major) && (p.Version.Minor == version.Minor) && (p.Version > version);*/
                    /*&& (p.Version.Minor == version.Minor) foi retirado, pois, quando é gerado um RTM o Minor é incrementado*/

                    PSE.Framework.Common.Version fileVersion = CreateVersion(p.Version);

                    return (fileVersion.Major == pseVersion.Major) && (fileVersion > pseVersion);
                });

                //se existem pacotes com versao superiora a informa
                if (availablePackages != null && availablePackages.Count > 0)
                {
                    List<IFileDetails> filteredPackages = new List<IFileDetails>();

                    //ordernar a lista de pacotes pela versao
                    //ATENCAO: mudar essa linha de codigo pode impactar em toda a logica do metodo
                    availablePackages.Sort(new Comparison<IFileDetails>(ComparePackagesVersion));

                    IFileDetails lastRTM = availablePackages.LastOrDefault(p =>
                    {
                        return p.FilePackageType == PackageType.RTM;
                    });

                    if (lastRTM == null)
                    {
                        //recuperar o ultimo SPK disponivel
                        var spks = availablePackages.Where(p =>
                        {
                            return p.FilePackageType == PackageType.ServicePack;
                        }).ToList();

                        IFileDetails lastSPK = null;
                        if (spks.Count > 0)
                        {
                            spks.Sort(new Comparison<IFileDetails>(ComparePackagesVersion));
                            lastSPK = spks.LastOrDefault();
                        }

                        //se existe um SPK
                        if (lastSPK != null)
                        {
                            PSE.Framework.Common.Version lastSPKVersion = CreateVersion(lastSPK.Version);

                            //recuperar todos os pacotes com versao superior ou igual a versao do SPK encontrado
                            filteredPackages = availablePackages.Where(p => 
                            {
                                PSE.Framework.Common.Version fileVersion = CreateVersion(p.Version);
                                if (p.FilePackageType.Value == PackageType.ServicePack)
                                    return false;

                                return fileVersion > lastSPKVersion;
                            }).ToList();

                            filteredPackages.Add(lastSPK);
                        }
                        //nao existe um SPK
                        else
                        {
                            filteredPackages = availablePackages;
                        }
                    }
                    else
                    {
                        //recuperar todos os pacotes com versao superior ou igual a versao do RTM encontrado
                        filteredPackages = availablePackages.FindAll(p =>
                        {
                            PSE.Framework.Common.Version fileVersion = CreateVersion(p.Version);
                            PSE.Framework.Common.Version lastRTMVersion = CreateVersion(lastRTM.Version);

                            return fileVersion >= lastRTMVersion;
                        });
                    }

                    //se algum item satisfaz as condicoes anteriores
                    if (filteredPackages != null && filteredPackages.Count > 0)
                    {
                        ///Para facilitar a manutenca do codigo, os passos de remover duplicados, validacao e criacao de pacotes faltando foram divididos em
                        ///tres metodos. É sabido que a performance seria melhor se tudo fosse realizado varrendo a colecao apenas uma vez, mas nesse caso
                        ///optou-se por um algoritmo mais simples de ser mantido em caso de necessidades de alterações.
                        ///A ordem de chamada dos métodos abaixo não deve ser alterada uma vez que a saída de um é usada como entrada de outro

                        //0 Reordena os itens
                        filteredPackages.Sort(new Comparison<IFileDetails>(ComparePackagesVersion));

                        //1 Remover itens duplicados
                        filteredPackages = RemoveDuplicatedPackages(filteredPackages);
                        //2 Validar sequencia
                        filteredPackages = ValidatePackageSequence(filteredPackages, pseVersion);
                        //3 Adicionar itens faltando, ou seja, caso hajam versoes faltando serao geradas informações de pacotes faltando
                        filteredPackages = CreateMissingPackages(filteredPackages, pseVersion);

                        return filteredPackages;

                    }
                    else //nao existem atualizacoes com versao superior a informada
                        return null;
                }
                else
                {
                    return null;
                }
            }
            else
            {
                DebugLog.Write("Nenhum pacote no cache com o módulo " + moduleName);
                return null;
            }
        }

        /// <summary>
        /// Transforma uma versão do tipo string para uma do tipo PSE
        /// </summary>
        /// <param name="version">Versão a ser transformada</param>
        /// <returns>Uma versão do Tipo PSE</returns>
        private PSE.Framework.Common.Version CreateVersion(string version)
        {
            string[] versionParts = version.Split('.');
            if (versionParts == null || versionParts.Length != 4)
                throw new ArgumentException("A versão informada não é valida");

            PSE.Framework.Common.Version pseVersion = new PSE.Framework.Common.Version(Convert.ToInt32(versionParts[0]), Convert.ToInt32(versionParts[1]), Convert.ToInt32(versionParts[2]), Convert.ToInt32(versionParts[3]));
            return pseVersion;
        }

        /// <summary>
        /// Criar os pacotes faltando na sequência
        /// </summary>
        /// <param name="clonePackages"></param>
        /// <returns></returns>
        private List<IFileDetails> CreateMissingPackages(List<IFileDetails> clonePackages, PSE.Framework.Common.Version currentVersion)
        {
            PSE.Framework.Common.Version localCurrentVersion = (PSE.Framework.Common.Version)currentVersion.Clone();

            if (clonePackages.Any(p => p.Status == PackageStatus.Invalid))
            {
                List<IFileDetails> retPackages = new List<IFileDetails>();

                for (int i = 0; i < clonePackages.Count; i++)
                {
                    IFileDetails missingPackage;
                    //se encontrar um pacote fora da ordem

                    var clonePackagesVersion = CreateVersion(clonePackages[i].Version);

                    if (localCurrentVersion.MajorRevision == clonePackagesVersion.MajorRevision &&
                       (localCurrentVersion.MinorRevision - 1) != clonePackagesVersion.MinorRevision)
                    {
                        //gerar os pacotes que estao faltando
                        short startVerion, endVersion;
                        startVerion = (short)(localCurrentVersion.MinorRevision + 1);
                        endVersion = clonePackagesVersion.MinorRevision;

                        for (short j = startVerion; j < endVersion; j++)
                        {

                            //fazer um clone do pacote atual
                            missingPackage = clonePackages[i].Clone();
                            missingPackage.Initialize(missingPackage.FilePath, ProviderType.Updates);
                            missingPackage.UpdateDescription = string.Empty;
                            missingPackage.Status = PackageStatus.Missing; //setar status como Missing
                            PSE.Framework.Common.Version version = new PSE.Framework.Common.Version(localCurrentVersion.ToString());
                            version.Revision = j;
                            missingPackage.UpdateDescription = "Missing...";
                            //missingPackage.Version.MinorRevision = j; //atribuir o minor revision
                            missingPackage.Version = version.ToString();
                            retPackages.Add(missingPackage);
                        }
                    }
                    //se mudou o major revision e o minor revision for diferente de 0
                    else if (localCurrentVersion.MajorRevision != clonePackagesVersion.MajorRevision &&
                                                                  clonePackagesVersion.MinorRevision != 0)
                    {
                        //remover todos os pacotes e indicar que esta faltando um spk
                        retPackages.Clear();

                        missingPackage = clonePackages[i].Clone();
                        missingPackage.Initialize(string.Empty, ProviderType.Updates);
                        missingPackage.UpdateDescription = string.Empty;
                        missingPackage.Status = PackageStatus.Missing; //setar status como Missing
                        PSE.Framework.Common.Version version = new PSE.Framework.Common.Version(0, 0, 0, 0);
                        missingPackage.Version = version.ToString();
                        
                        retPackages.Add(missingPackage);
                    }

                    retPackages.Add(clonePackages[i]);
                    localCurrentVersion = CreateVersion(clonePackages[i].Version);
                }
                return retPackages;
            }
            else
            {
                return clonePackages;
            }
        }

        /// <summary>
        /// Validar uma coleção de pacotes quanto a sequência de número de versão
        /// </summary>
        /// <param name="clonePackages"></param>
        /// <returns></returns>
        private List<IFileDetails> ValidatePackageSequence(List<IFileDetails> clonePackages, PSE.Framework.Common.Version currentVersion)
        {
            PSE.Framework.Common.Version localCurrentVersion = (PSE.Framework.Common.Version)currentVersion.Clone();

            for (int i = 0; i < clonePackages.Count; i++)
            {
                var clonePackagesVersion = CreateVersion(clonePackages[i].Version);
                if (localCurrentVersion.MajorRevision == clonePackagesVersion.MajorRevision &&
                   (localCurrentVersion.MinorRevision + 1) != clonePackagesVersion.MinorRevision)
                {
                    for (int j = i; j < clonePackages.Count; j++)
                    {
                        clonePackages[j].Status = PackageStatus.Invalid;
                    }
                    break;
                }

                localCurrentVersion = CreateVersion(clonePackages[i].Version);
            }
            return clonePackages;
        }

        /// <summary>
        /// Remover os pacotes duplicados.
        /// Pacotes duplicados são aqueles onde o Major,Minor e Revision da versao são iguais e o build diferente
        /// </summary>
        /// <param name="clonePackages">Coleção de pacotes da qual remover os duplicados</param>
        /// <returns></returns>
        private List<IFileDetails> RemoveDuplicatedPackages(List<IFileDetails> clonePackages)
        {
            List<IFileDetails> distinctPackages = new List<IFileDetails>();
            IFileDetails lastDiscintPackage;
            for (int i = clonePackages.Count - 1; i >= 0; i--)
            {
                //recuperar o ultimo item adicionado na lista de pacotes distintos
                lastDiscintPackage = distinctPackages.LastOrDefault();
                if (lastDiscintPackage == null)//se nao ha um item 
                    distinctPackages.Add(clonePackages[i]);
                else
                {
                    //se for a mesma revisao
                    //nao ha necessidade de compara Major e Minor pois sempre serao as mesmas
                    var lastDiscintPackageVersion = CreateVersion(lastDiscintPackage.Version);
                    var clonePackagesVersion = CreateVersion(clonePackages[i].Version);
                    if (lastDiscintPackageVersion.Revision == clonePackagesVersion.Revision)
                        continue;
                    else
                        distinctPackages.Add(clonePackages[i]);
                }
            }
            distinctPackages.Reverse();
            return distinctPackages;
        }

        /// <summary>
        /// Método que dois arquivos pela sua versão
        /// </summary>
        /// <param name="pd1">Arquivo 1</param>
        /// <param name="pd2">Arquivo 2</param>
        /// <returns></returns>
        private int ComparePackagesVersion(IFileDetails fileDetails1, IFileDetails fileDetails2)
        {
            var version1 = CreateVersion(fileDetails1.Version);
            var version2 = CreateVersion(fileDetails2.Version);

            return version1.CompareTo(version2);
        }
    }

    public class ComparePackage : IEqualityComparer<IFileDetails>
    {
        public bool Equals(IFileDetails x, IFileDetails y)
        {
            return x.SystemName.Equals(y.SystemName, StringComparison.InvariantCultureIgnoreCase) &&
                   x.ModuleName.Equals(y.ModuleName, StringComparison.InvariantCultureIgnoreCase) &&
                   x.Version.Equals(y.Version, StringComparison.InvariantCultureIgnoreCase);
        }

        public int GetHashCode(IFileDetails obj)
        {
            return base.GetHashCode();
        }
    }
}
